package bsh.meta;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;

import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

import bsh.CompilationState;
import bsh.exception.CompilationAssertionFailed;
import bsh.utils.reflection.BytecodeHelper;

/**
 * {@link TypeWrapper} para arrays
 * 
 * @author takeshi
 */
public class ArrayTypeWrapper extends AbstractTypeWrapper implements TypeWrapper {

    private final TypeWrapper componentType;

    /**
     * @param componentType o tipo componente do array
     */
    public ArrayTypeWrapper(TypeWrapper componentType) {
        super();
        this.componentType = componentType;
    }

    /**
     * {@inheritDoc}
     */
    public String className() {
        return "[" + this.componentType.internalName().replaceAll("[/]", ".");
    }

    /**
     * {@inheritDoc}
     */
    public String internalName() {
        return this.typeDescription();
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper superType() {
        return JavaClassWrapper.wrapperFor(Object.class);
    }

    /**
     * {@inheritDoc}
     */
    public String typeDescription() {
        return "[" + this.componentType.typeDescription();
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper getComponentType() {
        return this.componentType;
    }

    /**
     * Cria um novo array
     * 
     * @param state {@link CompilationState}
     * @param host {@link MethodVisitor}
     * @param length int
     */
    public void newArray(CompilationState state, MethodVisitor host, int length) {
        BytecodeHelper.pushIntConst(length, host);
        if (this.componentType.isPrimitive()) {
            int arrayType = this.findArrayType();
            host.visitIntInsn(Opcodes.NEWARRAY, arrayType);
        } else {
            host.visitTypeInsn(Opcodes.ANEWARRAY, this.componentType.internalName());
        }
        state.pushStack(this);
    }

    /**
     * Procura o opcode para criar um novo array
     * 
     * @return int
     */
    private int findArrayType() {
        if (this.componentType.equals(JavaClassWrapper.BOOLEAN_WRAPPER)) {
            return Opcodes.T_BOOLEAN;
        }
        if (this.componentType.equals(JavaClassWrapper.BYTE_WRAPPER)) {
            return Opcodes.T_BYTE;
        }
        if (this.componentType.equals(JavaClassWrapper.CHAR_WRAPPER)) {
            return Opcodes.T_CHAR;
        }
        if (this.componentType.equals(JavaClassWrapper.SHORT_WRAPPER)) {
            return Opcodes.T_SHORT;
        }
        if (this.componentType.equals(JavaClassWrapper.DOUBLE_WRAPPER)) {
            return Opcodes.T_DOUBLE;
        }
        if (this.componentType.equals(JavaClassWrapper.FLOAT_WRAPPER)) {
            return Opcodes.T_FLOAT;
        }
        if (this.componentType.equals(JavaClassWrapper.INT_WRAPPER)) {
            return Opcodes.T_INT;
        }
        if (this.componentType.equals(JavaClassWrapper.LONG_WRAPPER)) {
            return Opcodes.T_LONG;
        }
        return 0;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isSuperTypeOf(TypeWrapper another) {
        if (!(another instanceof ArrayTypeWrapper)) {
            return false;
        }
        TypeWrapper otherComponent = ((ArrayTypeWrapper) another).getComponentType();
        return this.componentType.isSuperTypeOf(otherComponent);
    }

    /**
     * {@inheritDoc}
     */
    public List<? extends TypeWrapper> interfaces() {
        return Arrays.asList(JavaClassWrapper.wrapperFor(Serializable.class), JavaClassWrapper.wrapperFor(Cloneable.class));
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends FieldDescriptor> visibleFields() {
        return Arrays.asList(new ArrayLengthField(this));
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends MethodDescriptor> visibleMethods() {
        return new LinkedList<MethodDescriptor>();
    }

    /**
     * {@link FieldDescriptor} para o 'campo' .length de um array
     * 
     * @author takeshi
     */
    private static final class ArrayLengthField implements FieldDescriptor {

        private final TypeWrapper declaring;

        /***
         * @param declaring {@link TypeWrapper}
         */
        public ArrayLengthField(TypeWrapper declaring) {
            super();
            this.declaring = declaring;
        }

        /**
         * {@inheritDoc}
         */
        public TypeWrapper declaringType() {
            return this.declaring;
        }

        /**
         * {@inheritDoc}
         */
        public TypeWrapper fieldType() {
            return JavaClassWrapper.INT_WRAPPER;
        }

        /**
         * {@inheritDoc}
         */
        public String fieldName() {
            return "length";
        }

        /**
         * {@inheritDoc}
         */
        public boolean isStatic() {
            return false;
        }

        /**
         * {@inheritDoc}
         */
        public int asmModifiers() {
            return Opcodes.ACC_PUBLIC + Opcodes.ACC_FINAL;
        }

        /**
         * {@inheritDoc}
         */
        public void writeGetValueCode(MethodVisitor visitor, CompilationState state) {
            state.assertMinimumStackSize(1);
            TypeWrapper wrapper = state.popStack();
            if (!(wrapper instanceof ArrayTypeWrapper)) {
                throw new CompilationAssertionFailed("cannot get array length on non array type: " + wrapper.className());
            }
            visitor.visitInsn(Opcodes.ARRAYLENGTH);
            state.pushStack(this.fieldType());
        }

        /**
         * {@inheritDoc}
         */
        public void writeWriteValueCode(MethodVisitor visitor, CompilationState state) {
            throw new CompilationAssertionFailed("cannot write array length");
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return this.internalName();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isInterface() {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends List<TypeWrapper>> visibleConstructors() {
        return Collections.emptyList();
    }

    /**
     * {@inheritDoc}
     */

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((this.componentType == null) ? 0 : this.componentType.hashCode());
        return result;
    }

    /**
     * {@inheritDoc}
     */

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj == null) {
            return false;
        }
        if (this.getClass() != obj.getClass()) {
            return false;
        }
        ArrayTypeWrapper other = (ArrayTypeWrapper) obj;
        if (this.componentType == null) {
            if (other.componentType != null) {
                return false;
            }
        } else if (!this.componentType.equals(other.componentType)) {
            return false;
        }
        return true;
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.TypeWrapper#protectedFields()
     */
    public Collection<? extends FieldDescriptor> protectedFields() {
        return Collections.emptyList();
    }

    /**
     * {@inheritDoc}
     * 
     * @see bsh.meta.TypeWrapper#protectedMethods()
     */
    public Collection<? extends MethodDescriptor> protectedMethods() {
        return Collections.emptyList();
    }

    /**
     * {@inheritDoc}
     */
    public Collection<? extends TypeWrapper> getInnerTypes() {
        return Collections.emptyList();
    }

    /**
     * {@inheritDoc}
     */
    public TypeWrapper getEnclosingType() {
        return null;
    }

}
